﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="StreetCareAuthorizeAttribute.cs" company="Spenta Consulting">
//   (c) Copyright 2011
// </copyright>
// <summary>
//   Allows you to decorate any method to enable authorization on it
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace Spenta.StreetCare.MVC.Helpers
{
    #region Using statements

    using System;
    using System.Collections.Specialized;
    using System.Linq;
    using System.Web;
    using System.Web.Mvc;

    using Microsoft.IdentityModel.Claims;

    using Spenta.StreetCare.Core.IoC;
    using Spenta.StreetCare.Core.Models;

    using AuthorizationContext = System.Web.Mvc.AuthorizationContext;

    #endregion

    /// <summary>
    /// Allows you to decorate any method to enable authorization on it
    /// </summary>
    public class StreetCareAuthorizeAttribute : AuthorizeAttribute
    {
        /// <summary>
        /// Private store for entities
        /// </summary>
        private StreetCareEntities entities;

        /// <summary>
        /// Gets the entities.
        /// </summary>
        protected StreetCareEntities Entities
        {
            get
            {
                return this.entities ?? (this.entities = new StreetCareEntities(IoCHelper.ConfigurationBag.GetConnectionString("StreetCareEntities")));
            }
        }

        #region Public methods

        /// <summary>
        /// Called when a process requests authorization.
        /// </summary>
        /// <param name="filterContext">The filter context, which encapsulates information for using <see cref="T:System.Web.Mvc.AuthorizeAttribute"/>.</param>
        /// <exception cref="T:System.ArgumentNullException">The <paramref name="filterContext"/> parameter is null.</exception>
        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            if (filterContext == null)
            {
                throw new ArgumentNullException("filterContext");
            }

            if (!this.AuthorizeCore(filterContext.HttpContext))
            {
                filterContext.Result = new HttpUnauthorizedResult();
                return;
            }

            // TODO: Should cache this
            var claims = this.GetClaims((ClaimsIdentity)filterContext.HttpContext.User.Identity);

            string identityProvider = claims["IdentityProvider"];
            string nameIdentifier = claims["NameIdentifier"];
            string name = claims["Name"] ?? "Default user";

            // The customerID has been obtained within the controller
            var customerID = ((Customer)filterContext.RouteData.Values["customer"]).CustomerID;

            User user = this.GetUser(identityProvider, nameIdentifier, name, customerID);

            // We store the customer in the Session object in order to retrieve it from the Silverlight component
            StoreUserInSession(filterContext, user);

            // If we don't require any role, just finish
            if (Roles.Equals(string.Empty))
            {
                // We only need to be authenticated
                this.SetCachePolicy(filterContext);
                return;
            }

            // if no other user is administrator on this customer, take ownership
            if (!this.Entities.Assignments.Where(a => a.AssignmentCustomerID == customerID && a.AssignmentRole.Equals("admin")).Any())
            {
                user.Assignments.Add(
                    new Assignment
                    {
                        AssignmentCustomerID = customerID,
                        AssignmentUserID = user.UserID,
                        AssignmentRole = "admin"
                    });

                this.Entities.SaveChanges();
            }

            // We need to find all the roles of this user. There should be always at least one role (automatically set above)
            var assignments = user.Assignments.Where(a => a.AssignmentUserID == user.UserID);
            if (assignments.Where(a => a.AssignmentRole.Equals("superadmin")).FirstOrDefault() != null)
            {
                // If the user is superadmin, allow always
                this.SetCachePolicy(filterContext);
                return;
            }

            // if the user is not a superadmin we need to ensure it has the appropiate role
            string[] split = Roles.Split(',');
            if (assignments.Where(a => a.AssignmentCustomerID == customerID).Any(assignment => split.Contains(assignment.AssignmentRole)))
            {
                this.SetCachePolicy(filterContext);
                return;
            }

            var viewData = new ViewDataDictionary
            {
                { "Message", "You do not have sufficient privileges for this operation." } 
            };

            filterContext.Result = new ViewResult { ViewName = "Error", ViewData = viewData };
        }

        /// <summary>
        /// Stores the user in session.
        /// </summary>
        /// <param name="filterContext">The filter context.</param>
        /// <param name="user">The user object</param>
        protected static void StoreUserInSession(ControllerContext filterContext, User user)
        {
            if (filterContext.HttpContext.Session != null)
            {
                if (filterContext.HttpContext.Session["StreetCareUser"] == null)
                {
                    filterContext.HttpContext.Session.Add("StreetCareUser", user);
                }
                else
                {
                    filterContext.HttpContext.Session["StreetCareUser"] = user;
                }
            }
        }

        /// <summary>
        /// Gets the user.
        /// </summary>
        /// <param name="identityProvider">The identity provider.</param>
        /// <param name="nameIdentifier">The name identifier.</param>
        /// <param name="name">The user name.</param>
        /// <param name="customerID">The customer ID.</param>
        /// <returns>
        /// The user with the given information
        /// </returns>
        protected User GetUser(string identityProvider, string nameIdentifier, string name, int customerID)
        {
            var user = this.Entities.Users.Where(
                u =>
                u.UserIdentityProvider.Equals(identityProvider, StringComparison.OrdinalIgnoreCase) &&
                u.UserNameIdentifier.Equals(nameIdentifier, StringComparison.OrdinalIgnoreCase)).
                FirstOrDefault();

            if (user == null)
            {
                // If we can't find any user with the information provide we create it here with user access
                user = new User
                {
                    UserIdentityProvider = identityProvider,
                    UserNameIdentifier = nameIdentifier,
                    UserNickname = name,
                    UserImageUrl = "/images/default/empty-simpleuser.png" // TODO: define somewhere
                };

                this.Entities.Users.AddObject(user);

                if (!this.Entities.Assignments.Where(a => a.AssignmentCustomerID == customerID && a.AssignmentRole.Equals("admin")).Any())
                {
                    user.Assignments.Add(
                        new Assignment
                        {
                            AssignmentCustomerID = customerID,
                            AssignmentUserID = user.UserID,
                            AssignmentRole = "admin"
                        });

                    this.Entities.SaveChanges();
                }
                else
                {
                    user.Assignments.Add(
                        new Assignment
                        {
                            AssignmentCustomerID = customerID,
                            AssignmentUserID = user.UserID,
                            AssignmentRole = "user"
                        });
                }

                this.Entities.SaveChanges();
            }

            return user;
        }

        #endregion

        #region Protected methods

        /// <summary>
        /// Gets the claims associated to the given identity.
        /// </summary>
        /// <param name="identity">The identity.</param>
        /// <returns>Collection of claims of the given identity</returns>
        protected NameValueCollection GetClaims(ClaimsIdentity identity)
        {
            var result = new NameValueCollection();

            var claims = identity.Claims;

            foreach (var claim in claims)
            {
                switch (claim.ClaimType)
                {
                    case "http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider":
                        result.Add("IdentityProvider", claim.Value);
                        break;
                    case "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier":
                        result.Add("NameIdentifier", claim.Value);
                        break;
                    case "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name":
                        result.Add("Name", claim.Value);
                        break;
                }
            }

            return result;
        }

        /// <summary>
        /// Sets the cache policy.
        /// </summary>
        /// <param name="filterContext">The filter context.</param>
        protected void SetCachePolicy(AuthorizationContext filterContext)
        {
            // ** IMPORTANT **     
            // Since we're performing authorization at the action level, the authorization code runs  
            // after the output caching module. In the worst case this could allow an authorized user   
            // to cause the page to be cached, then an unauthorized user would later be served the   
            // cached page. We work around this by telling proxies not to cache the sensitive page,    
            // then we hook our custom authorization code into the caching mechanism so that we have    
            // the final say on whether a page should be served from the cache.   
            HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
            cachePolicy.SetProxyMaxAge(new TimeSpan(0));
            cachePolicy.AddValidationCallback(this.CacheValidateHandler, null /* data */);
        }

        /// <summary>
        /// Caches the validate handler.
        /// </summary>
        /// <param name="context">The context.</param>
        /// <param name="data">The associated data.</param>
        /// <param name="validationStatus">The validation status.</param>
        protected void CacheValidateHandler(HttpContext context, object data, ref HttpValidationStatus validationStatus)
        {
            validationStatus = OnCacheAuthorization(new HttpContextWrapper(context));
        }

        /// <summary>
        /// Determines whether access to the core framework is authorized.
        /// </summary>
        /// <param name="httpContext">The HTTP context, which encapsulates all HTTP-specific information about an individual HTTP request.</param>
        /// <returns>
        /// true if access is authorized; otherwise, false.
        /// </returns>
        /// <exception cref="T:System.ArgumentNullException">The <paramref name="httpContext"/> parameter is null.</exception>
        protected override bool AuthorizeCore(HttpContextBase httpContext)
        {
            return httpContext.User.Identity.IsAuthenticated;
        }

        #endregion
    }
}